/*
  Autocomplete Widget
  Version: 1.0.0
  Author: Mike Thomas

  History:
  1.0 (Jan 8, 2008)
  - Nearly a complete rewrite.
  - Removed potential memory leaks.
  - Removed features that never really worked well and were never really used.

  0.9.1 (Nov 21, 2006)
  - Fixed a bug with the 'underMouse' variable and cleaned some things up

  0.9 (Nov 4, 2006)
  - Got 'select' mode working with a scrollbar

  0.8 (Oct 14, 2006)
  - Ajax part is not yet working and inline autocompletion is buggy. But pulling
    from a static list is working great.
  - Minor change, added a zIndex of 10 for the main dropdown so it is visible
    above other 'popups'
*/


function AutoComplete(elementId) {
  this.elementId = elementId;
  this.containerId = elementId + '-autocomplete';
  this.optionIdPrefix = this.containerId + '-option-';

  /* The array index of the highlighted option in this.visibleOptions */
  this.highlightedIndex = -1;

  /* Unfiltered list of options. */
  this.allOptions = [];

  /* Options matching the current user input */
  this.visibleOptions = [];

  /*
    Whether or not the mouse is currently over the menu. This is needed when
    the user clicks on the scrollbar in the menu so we know not to close.
  */
  this.underMouse = false;

  this.currValue = '';
  this.oldValue = '';
  this.timer = false;
  this.active = false;    // Keeps track of whether or not the dropdown is showing

  /* Setup default preferences */
  this.prefs = {};
  this.prefs.minChars = 1;
  this.prefs.maxResults = 15;
  this.prefs.fulltext = true;
  this.prefs.showKey = true;
  this.prefs.useKey = true;

  /* Setup the form element we're attaching to */
  var element = $(this.elementId);
  if (element.setAttribute) {
    element.setAttribute('autocomplete', 'off');
  }
  element.onkeydown = this.handleIgnoredKeyEvents.bindAsEventListener(this);
  element.onkeypress = this.handleIgnoredKeyEvents.bindAsEventListener(this);
  element.onkeyup = this.handleKeyUp.bindAsEventListener(this);
  element.onfocus = this.handleFocus.bindAsEventListener(this);
  element.onblur = this.handleBlur.bindAsEventListener(this);

  /* Setup global events */
  util.addEvent(window, 'resize', this.handleWindowResize.bind(this));
}

/*
  ------------------------------------------------------------------------------
  Event Handlers
  ------------------------------------------------------------------------------
*/

/*
  onkeydown/onkeypress handlers for the form element.
*/
AutoComplete.prototype.handleIgnoredKeyEvents = function(event) {
  if ([40, 38, 13].contains(event.keyCode)) {
    event.cancelBubble = true;
    event.returnValue = false;
    return false;
  }
  return true;
};

/*
  onkeyup handler for the form element.
*/
AutoComplete.prototype.handleKeyUp = function(event) {
  this.currValue = $(this.elementId).value;
  switch(event.keyCode) {
    case 40:    // Down Arrow
      if (!this.active && this.currValue.length >= this.prefs.minChars) {
        this.calculateVisibleOptions();
        this.show();
      }
      if (this.highlightedIndex < this.visibleOptions.length - 1) {
        if (this.highlightedIndex > -1) {
          this.unhighlightOption(this.highlightedIndex);
        }
        this.highlightedIndex++;
        this.highlightOption(this.highlightedIndex);
      }
      break;
    case 38:    // Up Arrow
      if (this.active && this.highlightedIndex > 0) {
        this.unhighlightOption(this.highlightedIndex);
        this.highlightedIndex--;
        this.highlightOption(this.highlightedIndex);
      }
      break;
    case 27:    // Escape
      if (this.active) {
        this.hide();
      }
      break;
    case 13:    // Enter
      if (this.active) {
        this.select();
      }
      break;
    default:
      break;
  }
  if ((event.keyCode == 40) || (event.keyCode == 38)) {
    event.cancelBubble = true;
    event.returnValue = false;
    return false;
  }
  return true;
};

/*
  onfocus handler for the form element.
*/
AutoComplete.prototype.handleFocus = function() {
  this.startTimer();
};

/*
  onblur handler for the form element.
*/
AutoComplete.prototype.handleBlur = function() {
  this.stopTimer();
  if (!this.underMouse && $(this.containerId)) {
    this.select();
  }
};

AutoComplete.prototype.handleWindowResize = function() {
  if (this.active) {
    var div = $(this.containerId);
    var element = $(this.elementId);
    var pos = util.findPos(element);
    div.style.left = pos[0] + 'px';
    div.style.top = (pos[1] + element.offsetHeight - 1) + 'px';
  }
};

/*
  onmouseout handler for the container
*/
AutoComplete.prototype.handleContainerMouseOut = function(event) {
  this.underMouse = false;
};

/*
  onmouseover handler for the container
*/
AutoComplete.prototype.handleContainerMouseOver = function(event) {
  this.underMouse = true;
};

AutoComplete.prototype.handleOptionMouseOver = function(event, index) {
  for (var i = 0; i < this.visibleOptions.length; i++) {
    this.unhighlightOption(i);
  }
  this.highlightOption(index);
};

AutoComplete.prototype.handleOptionClick = function(event) {
  this.select();
};

/*
  ------------------------------------------------------------------------------
  Working with options
  ------------------------------------------------------------------------------
*/

/*
  Given the index of an option, return the div that contains the option.
*/
AutoComplete.prototype.getOptionElement = function(index) {
  return $(this.optionIdPrefix + index);
};

AutoComplete.prototype.highlightOption = function(index) {
  this.highlightedIndex = index;
  var element = this.getOptionElement(index);
  element.className = element.className.replace('-hover', '');
  element.className += '-hover';
};

AutoComplete.prototype.unhighlightOption = function(index) {
  var element = this.getOptionElement(index);
  element.className = element.className.replace('-hover', '');
};

AutoComplete.prototype.setOptions = function(options) {
  this.allOptions = options;
};

/*
  ------------------------------------------------------------------------------
  Building the UI
  ------------------------------------------------------------------------------
*/

AutoComplete.prototype.buildOuterDiv = function() {
  var element = $(this.elementId);

  /* Get the position of the input element */
  var pos = util.findPos(element);
  var x = pos[0];
  var y = pos[1] + element.offsetHeight - 1;

  /* Create and style the outer div */
  if ($(this.containerId)) {
    var div = $(this.containerId);
  } else {
    var div = document.createElement('div');
    div.id = this.containerId;
    div.className = 'autocomplete-container';
    div.style.width = (element.offsetWidth - 2) + 'px';
    div.style.left = x + 'px';
    div.style.top = y + 'px';
    div.onmouseout = this.handleContainerMouseOut.bindAsEventListener(this);
    div.onmouseover = this.handleContainerMouseOver.bindAsEventListener(this); 
  }

  /* Make some adjustments for when we have > 20 options */
  if (this.visibleOptions.length > 20) {
    // TODO: Redo this via CSS
    div.style.height = '250px';
    div.style.overflow = 'auto';
  } else {
    div.style.height = '';
    div.style.overflow = '';
  }

  /* Clear any previous contents */
  div.innerHTML = '';

  return div;
};

AutoComplete.prototype.buildInnerDiv = function() {
  var element = $(this.elementId);

  /* Calculate the width of option divs */
  var optionWidth = element.offsetWidth - 6;
  if (this.visibleOptions.length > 20) {
    /* Compensate for width of scrollbar */
    optionWidth -= 19;
  }

  /* Create an inner div and populate it with options */
  var div = document.createElement('div');
  var count = 0;
  for (var i = 0; i < this.visibleOptions.length; i++) {
    var value = this.visibleOptions[i][0];
    var label = this.visibleOptions[i][1];
    var option = document.createElement('div');
    option.id = this.optionIdPrefix + count; 
    option.className = 'autocomplete-option';
    option.style.padding = '2px';
    option.style.width = optionWidth + 'px';
    option.innerHTML = label + (this.prefs.showKey ? ' (' + value + ')' : '');
    option.onmouseover = this.handleOptionMouseOver.bindAsEventListener(this,
        count);
    option.onclick = this.handleOptionClick.bindAsEventListener(this);
    div.appendChild(option);
    count++;
  }
  return div;
};

AutoComplete.prototype.show = function() {
  this.active = true;
  this.highlightedIndex = -1;
  if (this.visibleOptions.length > 0) {
    var outerDiv = this.buildOuterDiv();
    var innerDiv = this.buildInnerDiv();
    outerDiv.appendChild(innerDiv);
    document.getElementsByTagName('body')[0].appendChild(outerDiv);
  } else {
    this.hide();
  }
};

AutoComplete.prototype.hide = function() {
  this.active = false;
  this.underMouse = false;
  var element = $(this.containerId); 
  if (element && element.parentNode) {
    element.parentNode.removeChild(element);
  }
};

AutoComplete.prototype.startTimer = function() {
  this.timer = window.setTimeout(this.checkInput.bind(this), 100);
};

AutoComplete.prototype.stopTimer = function() {
  window.clearTimeout(this.timer);
  // TODO: May need a more definite way of stopping this.
};

AutoComplete.prototype.checkInput = function() {
  var element = $(this.elementId);
  if (element.value.length < this.prefs.minChars) {
    this.currValue = element.value;
    this.oldValue = element.value;
    this.hide();
  } else if (this.oldValue != this.currValue) {
    this.calculateVisibleOptions();
    this.show();
    this.oldValue = this.currValue;
  }
  this.startTimer();
};


AutoComplete.prototype.calculateVisibleOptions = function() {
  /* If they're typing forward, just search within the current results */
  if (this.oldValue && this.currValue.startsWith(this.oldValue)) {
    var optionsToFilter = this.visibleOptions.clone();
  } else {
    var optionsToFilter = this.allOptions.clone();
  }
  var currValue = this.currValue.toLowerCase();
  this.visibleOptions = [];
  /* Traverse backwards so we can remove elements that match as we go */
  for (var i = optionsToFilter.length - 1; i >= 0; i--) {
    var value = optionsToFilter[i][0].toLowerCase();
    var label = optionsToFilter[i][1].toLowerCase();
    if (value.startsWith(currValue) || label.startsWith(currValue)) {
      this.visibleOptions.unshift(optionsToFilter[i]);
      optionsToFilter.splice(i, 1);
    }
  }
  for (var i = 0; i < optionsToFilter.length; i++) {
    var value = optionsToFilter[i][0].toLowerCase();
    var label = optionsToFilter[i][1].toLowerCase();
    if (value.contains(currValue) || label.contains(currValue)) {
      this.visibleOptions.push(optionsToFilter[i]);
    }
  }
};

AutoComplete.prototype.select = function() {
  if ((this.highlightedIndex > -1) &&
      (this.highlightedIndex < this.visibleOptions.length)) {
    if (this.prefs.usekey) {
      $(this.elementId).value = this.visibleOptions[this.highlightedIndex][0];
    } else {
      $(this.elementId).value = this.visibleOptions[this.highlightedIndex][1];
    }
  }
  this.hide();
};
